#!/usr/bin/env python
# coding: utf-8

# In[110]:


get_ipython().run_line_magic('matplotlib', 'inline')
import numpy as np
import random
import torch
from torch.utils import data
import matplotlib.pyplot as plt
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"


# # 线性回归的简洁实现

# ## 准备批量化数据

# In[98]:


def synthetic_data(w, b, num_examples):  
    """生成y=Xw+b+噪声"""
    X = torch.normal(0, 1, (num_examples, len(w)))
    y = torch.matmul(X, w) + b
    y += torch.normal(0, 0.01, y.shape) # y.shape: torch.Size([1000,1])
    return X, y.reshape(-1,1) # y.shape: torch.Size([1000, 1])
    # return 为什么y要进行reshape？


# In[99]:


true_w = torch.tensor([2, -3.4])  # 给定真实的权重
true_b = 4.2                      # 给定真实的偏置
features, labels = synthetic_data(true_w, true_b, 1000)   # 生成特征矩阵与标签向量


# - 调用torch框架中现有的API来读取数据，并构造数据迭代器

# In[64]:


def load_array(data_arrays, batch_size, is_train=True):  
    """构造一个PyTorch数据迭代器
    is_train：是否在每个迭代周期打乱数据排序
    """
    dataset = data.TensorDataset(*data_arrays) # data_arrays: (features, labels)全数据样本
    return data.DataLoader(dataset, batch_size, shuffle=is_train) # torch.utils.data.dataloader.DataLoader


# - ```python
# from torch.utils import data
# data.DataLoader(dataset, batch_size=1, shuffle=False)
# ```
# 
#     - `dataset`：需要加载的数据集
#     - `batch_size`：一次加载的样本数量（批量），是`int`类型
#     - `shuffle`：每代（epoch）训练是否随机样本排序，是`bool`类型，降低对数据的过拟合
#     - 返回一个可迭代对象`iterable`

# - ```python
# data.TensorDataset(*tensors)
# ```
#     - `tensors`：一系列张量，这些张量的第一维度拥有相同的大小
#     - 类似于`Python`的`zip`函数

# In[7]:


a = torch.tensor([[1,2,4],[4,5,6],[7,8,9]])
b = torch.tensor([10,11,12])
c = data.TensorDataset(a,b)
c[0]


# In[100]:


batch_size = 10
data_iter = load_array((features, labels), batch_size)  # （features,labels）构成一个tuple作为load_array函数的第一个参数


# >【注意】data_iter并不是一个真的iterator, 要想当iterator来用，方法是：

# In[101]:


my_iter = iter(data_iter)  # Python内置的迭代器构造函数iter()
next(my_iter)


# ## 提出拟合数据的模型

# ### 使用框架的预定义好的**层**构建模型

# - 定义模型变量`net`，是`Sequential`类的一个实例

# - `Sequential`类将多个层**串联**在一起
#     - 当给定输入数据时，`Sequential`实例将数据传入到第一层，
#     - 然后将第一层的输出作为第二层的输入，以此类推。

# - 在`PyTorch`中，通过`Linear`类定义全连接层
# - 向`Linear`传递两个参数
#     - 输入特征形状，即2
#     - 输出特征形状，输出特征形状为单个标量，因此为1

# - ```python
# torch.nn.Linear(in_features,out_features,bias=True)
# ```
#     - `in_features`：输入样本的维度，`int`类型
#     - `out_features`：输出样本的维度，`int`类型
#     - `bias`：是否学习偏置参数，`bool`类型

# - 对输入数据$\mathbf{X}$进行线性变换
# 
# $$
# \boldsymbol{y} = \boldsymbol{w}\mathbf{X}+b
# $$

# - $\mathbf{X}$的形状为 (*, in_features)
# - $\boldsymbol{y}$的形状为（*，out_features)
# - *代表数据包含的样本数量

# - 权重$\boldsymbol{w}$的形状为(out_features,in_features)，权重初始值从均匀分布$U(-\sqrt{k},\sqrt{k})$中随机产生，其中$k=\tfrac{1}{\text {in_features}}$

# - 偏置$b$的形状为(out_features)，偏置的初始值也从均匀分布$U(-\sqrt{k},\sqrt{k})$中随机产生，其中$k=\tfrac{1}{\text{in_features}}$

# In[66]:


from torch import nn    ## nn是neural network

net = nn.Sequential(nn.Linear(2, 1))


# In[67]:


net


# ### 初始化模型参数

# - 深度学习框架通常用预定义的方法初始化参数

# - 初始化参数的方法
#     - 用`net[0]`选择网络的第一图层，如果是多层网络，用序号选择相应的图层
#     - `weight.data`访问权重参数，`bias.data`访问偏置参数
#     - `normal_()`和`fill_()`重新填写参数

# In[102]:


print(f'net网络的权重初始化从均值为0、标准差为0.01的正态分布随机采样，\
      \n{net[0].weight.data.normal_(0, 0.01)}')
print(f'net网络的偏置初始化为0，\n{net[0].bias.data.fill_(0)}')


# - 查看网络的参数
# 
# - ```python
# net.parameters()
# ```
# 
#     - `net`：建立的网络模型

# In[51]:


f'网络参数的类型为{type(net.parameters())}'
paras = net.parameters()  # 实例化对象
next(paras,'end')  # next()有一个默认参数，当迭代对象完毕的时候输出该默认参数，否则会报错
next(paras,'end')
next(paras,'end')


# ## 定义损失函数

# - 计算均方误差使用的是`torch.nn`的`MSELoss`类，也称为平方$L_2$范数
#     - 返回虽有样本损失的平均值

# In[103]:


loss = nn.MSELoss() # MeanSquaredError


# ## 定义优化算法

# - 小批量随机梯度下降算法（stochastic gradient descent, SGD）是一种优化神经网络的标准工具实例
# - `PyTorch`在`optim`模块中实现了该算法的许多变种

# - 使用的时候需要实例化一个`SGD`对象，同时指定参数
#     - 需要优化的参数，可以通过`net.parameters()`获得
#     - 算法的超参，例如学习速率`lr`

# In[104]:


trainer = torch.optim.SGD(net.parameters(), lr=0.03)


# ## 正式开始训练

# - 每个迭代周期将完整遍历一次数据集（`train_data`），不停地从中获取一个小批量的输入和相应的标签

# - 对于每一个小批量，我们会进行以下步骤:
#     * 通过调用`net(X)`生成预测并计算损失`l`（前向传播）
#     * 通过进行反向传播来计算梯度
#     * 通过调用优化器来更新模型参数

# In[97]:


yy = net(features)
yy.shape
labels.shape


# In[105]:


num_epochs = 10    # 10代训练
for epoch in range(num_epochs):
    for X, y in data_iter:  # 遍历所有数据
        trainer.zero_grad() # 梯度置0
        l = loss(net(X) ,y) # 正向计算损失，损失函数的参数为预测值和真值
        l.backward()        # 反向传播计算梯度
        trainer.step()      # 用SGD算法更新参数

    l = loss(net(features), labels) # 计算每代训练的损失，注意用全部数据
    print(f'epoch {epoch + 1}, loss {l:f}')


# - 比较生成数据集的真实参数和通过有限数据训练获得的模型参数

# In[106]:


w = net[0].weight.data
print(f'w的估计误差：, {true_w - w.reshape(true_w.shape)}')
b = net[0].bias.data
print(f'b的估计误差：, {true_b - b}')

